// Package utils
// Created by GoLand
// @User: lenora
// @Date: 2024/2/28
// @Time: 19:02

package httpclient

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"

	"go.elastic.co/apm/module/apmhttp/v2"

	"io"
	"log/slog"
	"net/http"
	"net/url"

	"golang.org/x/net/context"
)

type Request[T any] interface {
	Send(ctx context.Context) (*T, error)
	SetContentType(ct string) Request[T]
	SetSkipLog(skipLog bool) Request[T]
	AddHeader(Header map[string]string) Request[T]
	WithBasicAuth(username, password string) Request[T]
	WithParam(key, value string) Request[T]
}

func NewRequest[T any](method string, url string, body any) Request[T] {
	return &requestImpl[T]{Method: method, URL: url, Body: body}
}

type requestImpl[T any] struct {
	Method      string            `json:"method,omitempty"`
	URL         string            `json:"url,omitempty"`
	Header      map[string]string `json:"-"`
	Body        any               `json:"body,omitempty"`
	ContentType string            `json:"-"`
	SkipLog     bool              `json:"-"`
	BasicAuth   *basicAuth        `json:"-"`
	Params      url.Values        `json:"params,omitempty"`
}

type basicAuth struct {
	UserName string
	Password string
}

func (r *requestImpl[T]) AddHeader(Header map[string]string) Request[T] {
	if r.Header == nil {
		r.Header = make(map[string]string)
	}
	for k, v := range Header {
		r.Header[k] = v
	}

	return r
}

func (r *requestImpl[T]) SetContentType(ct string) Request[T] {
	r.ContentType = ct
	return r
}

func (r *requestImpl[T]) SetSkipLog(skipLog bool) Request[T] {
	r.SkipLog = skipLog
	return r
}

// WithBasicAuth sets basic authentication for the request.
func (r *requestImpl[T]) WithBasicAuth(username, password string) Request[T] {
	r.BasicAuth = &basicAuth{
		UserName: username,
		Password: password,
	}

	return r
}

func (r *requestImpl[T]) WithParam(key, value string) Request[T] {
	if r.Params == nil {
		r.Params = url.Values{}
	}

	r.Params.Set(key, value)

	return r
}

func (r *requestImpl[T]) Send(ctx context.Context) (*T, error) {
	var (
		req *http.Request
		err error
	)

	url := r.URL
	if r.Params != nil {
		url = fmt.Sprintf("%s?%s", url, r.Params.Encode())
	}

	switch r.Method {
	case http.MethodGet:
		req, err = http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	case http.MethodPost, http.MethodDelete, http.MethodPut, http.MethodPatch:
		var bodyBytes []byte
		switch v := r.Body.(type) {
		case []byte:
			bodyBytes = v
		case string:
			bodyBytes = []byte(v)
		default:
			var err error
			bodyBytes, err = json.Marshal(r.Body)
			if err != nil {
				return nil, newHTTPError(err)
			}
		}
		req, err = http.NewRequestWithContext(ctx, r.Method, url, bytes.NewBuffer(bodyBytes))
	default:
		err = errors.New("wrong method")
	}
	if err != nil {
		return nil, newHTTPError(err)
	}

	if r.Header != nil {
		for k, v := range r.Header {
			req.Header.Add(k, v)
		}
	}
	if r.ContentType != "" {
		req.Header.Add("Content-Type", r.ContentType)
	}

	if r.BasicAuth != nil {
		req.SetBasicAuth(r.BasicAuth.UserName, r.BasicAuth.Password)
	}

	client := apmhttp.WrapClient(http.DefaultClient)
	resp, err := client.Do(req)
	defer func() {
		if resp != nil && resp.Body != nil {
			_ = resp.Body.Close()
		}
	}()
	if err != nil {
		slog.Error("[HTTP] send error", "request", r, "error", err)
		return nil, newHTTPError(err)
	}

	var respBodyBytes []byte
	if resp.Body != nil {
		respBodyBytes, err = io.ReadAll(resp.Body)
		if err != nil {
			return nil, newHTTPError(err)
		}
	}

	var respContent any
	if _, ok := any(*new(T)).([]byte); ok {
		respContent = &respBodyBytes
	} else {
		respContent = new(T)
		err = json.Unmarshal(respBodyBytes, respContent)

	}

	if resp.StatusCode >= http.StatusBadRequest {
		slog.Error("[HTTP] code error", "request", r, "status", resp.Status)
		return nil, newHTTPErrorWithStatusCode(resp.StatusCode, errors.New(resp.Status), respContent)
	}

	if err != nil {
		slog.Error("[HTTP] unmarshal error", "request", r, "error", err, "response", string(respBodyBytes))
		return nil, newHTTPError(err)
	}

	if !r.SkipLog {
		slog.Info("[HTTP] success", "request", r, "response", respContent)
	}

	return respContent.(*T), nil
}
